# Download podcasts.
#

import threading
import network
import time
import db
import heapq
import index
import traceback
from feed import Feed
from log import log

# Do not poll for feed updates more than once every xx seconds:
FEED_UPDATE_PERIOD = 3600
NUM_DOWNLOAD_THREADS = 1
uptodate_feeds = []
uptodate_feeds_lock = threading.Lock()
waiting_feeds = []
waiting_feeds_cond = threading.Condition()

class FeedParseTimeComparator:
  def __init__(self, feed):
    self._feed = feed

  def __cmp__(self, other):
    if self._feed.last_parsed() < other._feed.last_parsed():
      return -1
    if self._feed.last_parsed() == other._feed.last_parsed():
      return 0
    return 1

  def __repr__(self):
    return "<FeedParseTimeComparator for feed %s>" % self._feed.feed_id()

  def feed(self):
    return self._feed

def Start():
  log("Creating", NUM_DOWNLOAD_THREADS, "download threads")
  for i in range(NUM_DOWNLOAD_THREADS):
    NewDownloadThread("Download%d" % i)

def AddFeed(feed):
  global waiting_feeds_cond
  global waiting_feeds

  # Add to waiting feeds and wake up any download thread that might be able to
  # handle it.
  waiting_feeds_cond.acquire()
  heapq.heappush(waiting_feeds, FeedParseTimeComparator(feed))
  waiting_feeds_cond.notify()
  waiting_feeds_cond.release()

def FeedInQueue(queue, feed):
  """You must already have the cond locked."""
  global waiting_feeds
  for wfeed in queue:
    if wfeed == feed:
      return True
  return False

def RemoveFeed(feed):
  global uptodate_feeds_lock
  global uptodate_feeds
  global waiting_feeds_cond
  global waiting_feeds

  log("UNSUBSCRIBE REQUEST:",feed)
  # If the feed is currently being downloaded then setting this flag true will
  # prevent it from being added back into the pool of uptodate/waiting feeds.
  # The next time it is dequeued it will be dropped.
  waiting_feeds_cond.acquire()
  uptodate_feeds_lock.acquire()

  if FeedInQueue(waiting_feeds, feed) or FeedInQueue(uptodate_feeds, feed):
    # If it is in one of the queues then we can delete all its data right now. A
    # reference to it will remain in the queue, but it will be thrown out when
    # dequeued.
    feed.Remove()
  feed.set_unsubscribing(True)

  uptodate_feeds_lock.release()
  waiting_feeds_cond.release()

def NewDownloadThread(name):
  thread = threading.Thread(name=name)
  thread.run = DownloadThread
  thread.start()

def DownloadThread():
  log("Download thread starting - sleeping for 1 hour to avoid annoying the developers")
  time.sleep(3600)
  log("Download thread starting")
  while True:
    try:
      network.WaitForConnection()
      log("Trying to get a feed")
      feed = GetFeed()
      log("Got feed", feed)
      if feed.valid():
        # If the feed is invalid then it's been deleted. We won't call ReturnFeed
        # and it will never be seen or heard from again.
        if feed.Parse():
          feed.DownloadEpisodes()
        log("Returning feed", feed)
        ReturnFeed(feed)
    except:
      log("Uncaught exception in DownloadThread")
      log(traceback.format_exc())

def UpToDateSecondsLeft():
  """Return the number of seconds until the least-fresh up-to-date feed becomes
  stale."""
  global uptodate_feeds
  global uptodate_feeds_lock
  uptodate_feeds_lock.acquire()
  if not uptodate_feeds:
    # Just wait a long time. There are no up to date feeds.
    seconds_left = FEED_UPDATE_PERIOD
  else:
    age = time.time() - time.mktime(
        uptodate_feeds[0].feed().last_parsed().timetuple())
    seconds_left = FEED_UPDATE_PERIOD - age
  uptodate_feeds_lock.release()
  return seconds_left

def MoveStaleFeedsToWaiting():
  """Move stale feeds out of uptodate and into the waiting queue."""
  global waiting_feeds_cond
  global waiting_feeds
  global uptodate_feeds
  global uptodate_feeds_lock

  now = time.time()
  waiting_feeds_cond.acquire()
  uptodate_feeds_lock.acquire()
  stale_indices = []
  new_waiting = 0
  while (len(uptodate_feeds) and
         not uptodate_feeds[0].feed().UpToDate(FEED_UPDATE_PERIOD, now)):
    feed = heapq.heappop(uptodate_feeds).feed()
    if not feed.unsubscribing():
      log("Feed",feed,"is stale. Moving to waiting queue.")
      heapq.heappush(waiting_feeds, FeedParseTimeComparator(feed))
      new_waiting += 1
    else:
      log("Dropping unsubscribed feed that became stale", feed)
  uptodate_feeds_lock.release()

  # Wake up the download threads just in case they're sleeping too long. Wake
  # up only as many threads as there are waiting feeds, and don't try to wake
  # up more threads than actually exist.
  for i in range(min(new_waiting, NUM_DOWNLOAD_THREADS)):
    waiting_feeds_cond.notify()

  waiting_feeds_cond.release()

def GetFeed():
  global waiting_feeds_cond
  global waiting_feeds

  feed = None
  # Loop until a good feed is found (that is, one that isn't being unsubscribed).
  while feed is None:
    waiting_feeds_cond.acquire()
    log("Waiting feeds:", waiting_feeds)
    while not waiting_feeds:
      log("No waiting feeds. Sleeping for up to", UpToDateSecondsLeft())
      waiting_feeds_cond.wait(UpToDateSecondsLeft())
      waiting_feeds_cond.release()
      MoveStaleFeedsToWaiting()
      waiting_feeds_cond.acquire()
    feed = heapq.heappop(waiting_feeds).feed()
    if feed.unsubscribing():
      # Got a bum feed: it's already been marked as unsubscribing. Just drop the
      # reference and try to find another.
      log("GetFeed found an unsusbscribing feed", feed, "...trying again")
      feed = None
    waiting_feeds_cond.release()
  return feed

def ReturnFeed(feed):
  if feed.UpToDate(FEED_UPDATE_PERIOD):
    global uptodate_feeds_lock
    global uptodate_feeds
    uptodate_feeds_lock.acquire()
    if feed.unsubscribing():
      log("ReturnFeed dropping an unsusbscribing feed", feed)
      feed.Remove()
    elif feed.valid():
      heapq.heappush(uptodate_feeds, FeedParseTimeComparator(feed))
    uptodate_feeds_lock.release()
  else:
    global waiting_feeds_cond
    global waiting_feeds
    waiting_feeds_cond.acquire()
    if feed.unsubscribing():
      log("ReturnFeed dropping an unsusbscribing feed", feed)
      feed.Remove()
    elif feed.valid():
      heapq.heappush(waiting_feeds, FeedParseTimeComparator(feed))
    waiting_feeds_cond.release()

